﻿using System;
using System.Text;
using System.Text.RegularExpressions;
using DictObj = System.Collections.Generic.Dictionary<string, object>;
using ListObj = System.Collections.Generic.List<object>;

namespace SebWindowsConfig.Utilities
{
	public class SEBURLFilter
	{
		public bool enableURLFilter;
		public bool enableContentFilter;
		public ListObj permittedList = new ListObj();
		public ListObj prohibitedList = new ListObj();

		// Updates filter rule arrays with current settings
		public void UpdateFilterRules()
		{
			if (prohibitedList.Count != 0)
			{
				prohibitedList.Clear();
			}

			if (permittedList.Count != 0)
			{
				permittedList.Clear();
			}

			enableURLFilter = (bool)SEBSettings.settingsCurrent[SEBSettings.KeyURLFilterEnable];
			enableContentFilter = (bool)SEBSettings.settingsCurrent[SEBSettings.KeyURLFilterEnableContentFilter];

			// Add global URLFilterRules
			ListObj URLFilterRules = (ListObj)SEBSettings.settingsCurrent[SEBSettings.KeyURLFilterRules];
			ReadURLFilterRules(URLFilterRules);

			// Add URLFilterRules from additional resources
			ListObj additionalResources = (ListObj)SEBSettings.settingsCurrent[SEBSettings.KeyAdditionalResources];
			ReadFilterRulesFromAdditionalResources(additionalResources);

			// If URL filtering is enabled, then
			// check if Start URL gets allowed by current filter rules and if not add a rule for the Start URL
			string startURLString = (string)SEBSettings.settingsCurrent[SEBSettings.KeyStartURL];

			if (enableURLFilter && Uri.TryCreate(startURLString, UriKind.Absolute, out Uri startURL))
			{
				if (TestURLAllowed(startURL) != URLFilterRuleActions.allow)
				{
					SEBURLFilterRegexExpression expression;
					// If Start URL is not allowed: Create one using the full Start URL
					try
					{
						expression = new SEBURLFilterRegexExpression(startURLString);
					}
					catch (Exception ex)
					{
						Logger.AddError("Could not create SEBURLFilterRegexExpression: ", this, ex, ex.Message);
						prohibitedList.Clear();
						permittedList.Clear();
						// Convert these rules and add them to the XULRunner seb keys
						CreateSebRuleLists();
						return;
					}

					// Add this Start URL filter expression to the permitted filter list
					permittedList.Add(expression);

				}
			}
			// Convert these rules and add them to the XULRunner seb keys
			CreateSebRuleLists();
		}


		public void ReadURLFilterRules(ListObj URLFilterRules)
		{
			foreach (DictObj URLFilterRule in URLFilterRules)
			{

				if (URLFilterRule.ContainsKey(SEBSettings.KeyURLFilterRuleRegex) && (bool)URLFilterRule[SEBSettings.KeyURLFilterRuleActive] == true)
				{

					string expressionString = (string)URLFilterRule[SEBSettings.KeyURLFilterRuleExpression];
					if (!String.IsNullOrEmpty(expressionString))
					{
						Object expression;

						bool regex = (bool)URLFilterRule[SEBSettings.KeyURLFilterRuleRegex];
						try
						{
							if (regex)
							{
								expression = new Regex(expressionString, RegexOptions.IgnoreCase);
							}
							else
							{
								expression = new SEBURLFilterRegexExpression(expressionString);
							}
						}
						catch (Exception ex)
						{
							Logger.AddError("Could not create SEBURLFilterRegexExpression: ", this, ex, ex.Message);
							prohibitedList.Clear();
							permittedList.Clear();
							throw;
						}

						int action = (int)URLFilterRule[SEBSettings.KeyURLFilterRuleAction];
						switch (action)
						{

							case (int)URLFilterRuleActions.block:

								prohibitedList.Add(expression);
								break;


							case (int)URLFilterRuleActions.allow:

								permittedList.Add(expression);
								break;
						}
					}
				}
			}
		}


		// Read URLFilterRules from additionalResources
		public void ReadFilterRulesFromAdditionalResources(ListObj additionalResources)
		{
			foreach (DictObj additionalResource in additionalResources)
			{
				if ((bool)additionalResource[SEBSettings.KeyAdditionalResourcesActive])
				{
					object URLFilterRules;
					if (additionalResource.TryGetValue(SEBSettings.KeyURLFilterRules, out URLFilterRules))
					{
						ReadURLFilterRules((ListObj)URLFilterRules);
					}

					// Are there further additional resources in this additional resource?
					if (additionalResource.TryGetValue(SEBSettings.KeyAdditionalResources, out object additionalSubResources))
					{
						if (((ListObj)additionalSubResources).Count != 0)
						{
							ReadFilterRulesFromAdditionalResources((ListObj)additionalSubResources);
						}
					}
				}
			}
		}

		// Convert these rules and add them to the XULRunner seb keys
		public void CreateSebRuleLists()
		{
			// Set prohibited rules
			SEBSettings.settingsCurrent[SEBSettings.KeyUrlFilterBlacklist] = SebRuleStringForSEBURLFilterRuleList(prohibitedList);

			// Set permitted rules
			SEBSettings.settingsCurrent[SEBSettings.KeyUrlFilterWhitelist] = SebRuleStringForSEBURLFilterRuleList(permittedList);

			// All rules are regex
			SEBSettings.settingsCurrent[SEBSettings.KeyUrlFilterRulesAsRegex] = true;

			// Set if content filter is enabled
			SEBSettings.settingsCurrent[SEBSettings.KeyUrlFilterTrustedContent] = !(bool)SEBSettings.settingsCurrent[SEBSettings.KeyURLFilterEnableContentFilter];
		}


		public string SebRuleStringForSEBURLFilterRuleList(ListObj filterRuleList)
		{
			if (filterRuleList.Count == 0)
			{
				// No rules defined
				return "";
			}

			StringBuilder sebRuleString = new StringBuilder();
			foreach (object expression in filterRuleList)
			{
				if (expression != null)
				{
					if (sebRuleString.Length == 0)
					{
						sebRuleString.Append(expression.ToString());
					}
					else
					{
						sebRuleString.AppendFormat(";{0}", expression.ToString());
					}
				}
			}

			return sebRuleString.ToString();
		}


		// Filter URL and return if it is allowed or blocked
		public URLFilterRuleActions TestURLAllowed(Uri URLToFilter)
		{
			string URLToFilterString = URLToFilter.ToString();
			// By default URLs are blocked
			bool allowURL = false;
			bool blockURL = false;

			/// Apply current filter rules (expressions/actions) to URL
			/// Apply prohibited filter expressions

			foreach (object expression in prohibitedList)
			{

				if (expression.GetType().Equals(typeof(Regex)))
				{
					if (Regex.IsMatch(URLToFilterString, expression.ToString()))
					{
						blockURL = true;
						break;
					}
				}

				if (expression.GetType().Equals(typeof(SEBURLFilterRegexExpression)))
				{
					if (URLMatchesFilterExpression(URLToFilter, (SEBURLFilterRegexExpression)expression))
					{
						blockURL = true;
						break;
					}
				}
			}
			if (blockURL == true)
			{
				return URLFilterRuleActions.block;
			}

			/// Apply permitted filter expressions

			foreach (object expression in permittedList)
			{

				if (expression.GetType().Equals(typeof(Regex)))
				{
					if (Regex.IsMatch(URLToFilterString, expression.ToString()))
					{
						allowURL = true;
						break;
					}
				}

				if (expression.GetType().Equals(typeof(SEBURLFilterRegexExpression)))
				{
					if (URLMatchesFilterExpression(URLToFilter, (SEBURLFilterRegexExpression)expression))
					{
						allowURL = true;
						break;
					}
				}

			}
			// Return URLFilterActionAllow if URL is allowed or
			// URLFilterActionUnknown if it's unknown (= it will anyways be blocked)
			return allowURL ? URLFilterRuleActions.allow : URLFilterRuleActions.unknown;
		}

		// Method comparing all components of a passed URL with the filter expression
		// and returning YES (= allow or block) if it matches
		public bool URLMatchesFilterExpression(Uri URLToFilter, SEBURLFilterRegexExpression filterExpression)
		{
			Regex filterComponent;

			// If a scheme is indicated in the filter expression, it has to match
			filterComponent = filterExpression.scheme;
			UriBuilder urlToFilterParts = new UriBuilder(URLToFilter);

			if (filterComponent != null &&
				!Regex.IsMatch(URLToFilter.Scheme, filterComponent.ToString(), RegexOptions.IgnoreCase))
			{
				// Scheme of the URL to filter doesn't match the one from the filter expression: Exit with matching = NO
				return false;
			}

			string userInfo = URLToFilter.UserInfo;
			filterComponent = filterExpression.user;
			if (filterComponent != null && 
				!Regex.IsMatch(urlToFilterParts.UserName, filterComponent.ToString(), RegexOptions.IgnoreCase))
			{
				return false;
			}

			filterComponent = filterExpression.password;
			if (filterComponent != null && 
				!Regex.IsMatch(urlToFilterParts.Password, filterComponent.ToString(), RegexOptions.IgnoreCase))
			{
				return false;
			}

			filterComponent = filterExpression.host;
			if (filterComponent != null && 
				!Regex.IsMatch(URLToFilter.Host, filterComponent.ToString(), RegexOptions.IgnoreCase))
			{
				return false;
			}

			if (filterExpression.port != null && URLToFilter.Port != filterExpression.port)
			{
				return false;
			}

			filterComponent = filterExpression.path;
			if (filterComponent != null && 
				!Regex.IsMatch(URLToFilter.AbsolutePath.TrimEnd(new char[] { '/' }), filterComponent.ToString(), RegexOptions.IgnoreCase))
			{
				return false;
			}

			string urlQuery = URLToFilter.GetComponents(UriComponents.Query, UriFormat.Unescaped);
			filterComponent = filterExpression.query;
			if (filterComponent != null)
			{
				// If there's a query filter component, then we need to even filter empty URL query strings
				// as the filter might either allow some specific queries or no query at all ("?." query filter)
				if (urlQuery == null)
				{
					urlQuery = "";
				} 
				if (!Regex.IsMatch(urlQuery, filterComponent.ToString(), RegexOptions.IgnoreCase))
				{
					return false;
				}
			}

			string urlFragment = URLToFilter.GetComponents(UriComponents.Fragment, UriFormat.Unescaped);
			filterComponent = filterExpression.fragment;
			if (filterComponent != null &&
				!Regex.IsMatch(urlFragment, filterComponent.ToString(), RegexOptions.IgnoreCase))
			{
				return false;
			}

			// URL matches the filter expression
			return true;
		}

	}
}
